﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Reflection;

using PostSharp.Aspects;
using BizElements.Core;
using BizElements.BusinessLayer;

namespace BizElements.BusinessLayer.Aspects
{
    #region Documentation.
    /// <summary>
    /// Marks a method which will be executed within a shared or local <b>BizElements</b> transaction provided by <b>IConnectionProvider</b>.
    /// </summary>
    /// <remarks><para>During the build process the code, the code which manages local or shared transaction, is automatically injected and intercepts
    /// the method marked with <b>TransactionRequiredAttribute</b> attribute.</para>
    /// <para>The attribute supports the following two cases:
    /// <list type="bullet">
    /// <item>
    ///     <term>Static method.</term>
    ///     <description>One of its arguments must be a <b>IConnectionProvider</b> object.</description></item>
    /// <item>
    ///     <term>Instance method.</term>
    ///     <description>First, the attribute will try to retrieve a <b>IConnectionProvider</b> object from the method's argument list.
    /// If that fails it will try to access the class property specified in the <see cref="ConnectionProviderPropertyName"/>.</description></item>
    /// </list>
    /// </para>
    /// <para>To use this attribute you have to install PostSharp 2 on your development computer and add references to <b>PostSharp.Aspects</b> and <b>PostSharp.Public</b> assemblies to your project.</para>
    /// </remarks>
    /// <example>The following example demonstrates how to use the attribute:
    /// <code>
    /// [TransactionRequired]
    /// public void Save()
    /// {
    ///     DAL.GetDao(this, _ConnectionProvider).Save();
    ///     DAL.GetDao(this.Children, _ConnectionProvider).Save();
    /// }
    /// </code>
    /// The previous example is functionally identical to the following pattern:
    /// <code>
    /// public void Save()
    /// {
    ///     bool isLocalTran = OrmUtil.RequestTransaction(_ConnectionProvider);
    ///     try
    ///     {
    ///         DAL.GetDao(this, _ConnectionProvider).Save();
    ///         DAL.GetDao(this.Children, _ConnectionProvider).Save();
    ///         OrmUtil.CommitTransactionIfLocal(_ConnectionProvider, isLocalTran);
    ///     }
    ///     finally
    ///     {
    ///         OrmUtil.CloseConnectionIfLocal(_ConnectionProvider, isLocalTran);
    ///     }
    /// }
    /// </code>
    /// <para><b>TransactionRequiredAttribute</b> can be used together with other attributes. <b>TransactionRequiredAttribute</b> has lower lower aspect 
    /// priority than <b>RemoteableAttribute</b> ensuring that the injected code is executed only once, ie. only in domain where method's body is executed.
    /// The following example demonstrates how the attributes are combined:
    /// <code>
    /// [Remoteable, InvalidatesCache, LogTransaction, TransactionRequired]
    /// public virtual void Save(IActor actionInitiator)
    /// {
    ///     DAL.GetDao(this, _ConnectionProvider).Save();
    ///     DAL.GetDao(this.Children, _ConnectionProvider).Save();
    /// }
    /// </code>
    /// The previous example is functionally identical to the following pattern:
    /// <code>
    /// public void Save(IActor actionInitiator)
    /// {
    ///     RpcHelper.InvokeMethod&lt;IActor&gt;(this, DbSave, actionInitiator);
    ///     GlobalCache.RemoveCategory("Countries");
    /// }
    /// 
    /// protected virtual void DbSave(IActor actionInitiator)
    /// {
    ///     bool isLocalTran = OrmUtil.RequestTransaction(_ConnectionProvider);
    ///     try
    ///     {
    ///         DAL.GetDao(this, _ConnectionProvider).Save();
    ///         DAL.GetDao(this.Children, _ConnectionProvider).Save();
    ///         OrmUtil.CommitTransactionIfLocal(_ConnectionProvider, isLocalTran);
    ///     }
    ///     finally
    ///     {
    ///         OrmUtil.CloseConnectionIfLocal(_ConnectionProvider, isLocalTran);
    ///     }
    ///     
    ///     LogHelper.LogTransaction("Country.Save", /*no description*/ null, actionInitiator, this);
    ///     GlobalCache.RemoveCategory("Countries");
    /// }
    /// </code>
    /// </para>
    /// </example>
    #endregion
    [Serializable]
    [AttributeUsage(AttributeTargets.Method)]
    public sealed class TransactionRequiredAttribute : OnMethodBoundaryAspect
    {
        #region Configuration.

        /// <summary>
        /// Gets the default value for <see cref="ConnectionProviderPropertyName"/> property.
        /// </summary>
        /// <value>A string configured in application settings. Default is <b>"_ConnectionProvider"</b>.</value>
        /// <remarks>Default value is configured in the <b>appSettings</b> section of an application's configuration file:
        /// <code><add key="BizElements.BusinessLayer.Aspects.TransactionRequiredAttribute.DefaultConnectionProviderPropertyName" value="_ConnectionProvider"/></code>
        /// </remarks>
        public static string DefaultConnectionProviderPropertyName { get; private set; }

        static TransactionRequiredAttribute()
        {
            // Static CTor is executed in runtime.
            string cfgDefaultConnectionProviderPropertyName = ConfigurationManager.AppSettings["BizElements.BusinessLayer.Aspects.TransactionRequiredAttribute.DefaultConnectionProviderPropertyName"];

            DefaultConnectionProviderPropertyName = cfgDefaultConnectionProviderPropertyName ?? "_ConnectionProvider";
        }

        #endregion

        #region CTor.

        /// <summary>
        /// Initializes a new instance of <b>TransactionRequiredAttribute</b> class and sets its <b>AspectPriority</b>.
        /// </summary>
        public TransactionRequiredAttribute()
        {
            // CTor is executed only during postcompilation.
            // Lower priority than Remoteable to ensure that the transaction is started only in the remote domain.
            this.AspectPriority = 100;
            this.ThrowExceptionIfConnectionProviderCannotBeRetrieved = true;
        }

        #endregion

        #region Properties.

        /// <summary>
        /// Gets or sets whether an <see cref="InvalidOperationException"/> is generated if an instance of <b>IConnectionProvider</b> cannot be retrieved.
        /// </summary>
        /// <value><b>true</b> to generate exception; <b>false</b> otherwise. Default is <b>true</b>.</value>
        public bool ThrowExceptionIfConnectionProviderCannotBeRetrieved { get; set; }

        // Property cannot be initalized with default value in ctor because the attribute in not initialized during runtime, but in compilation time.
        string connectionProviderPropertyName;

        /// <summary>
        /// Gets or sets the name of a non-static class property which exposes a <b>IConnectionProvider</b> that provides transactions for instances of the current class.
        /// </summary>
        /// <value>A string which represents a property name. Default value is set in <see cref="DefaultConnectionProviderPropertyName"/>.</value>
        public string ConnectionProviderPropertyName
        {
            get { return this.connectionProviderPropertyName ?? DefaultConnectionProviderPropertyName; }
            set { this.connectionProviderPropertyName = value; }
        }

        #endregion

        #region OnMethodBoundaryAspect overrides.

        /// <summary>
        /// Ensures the transaction is running. Does nothing if transaction is shared; otherwise it starts a new local transcation. Executed <b>before</b> the body of methods to which this aspect is applied. 
        /// </summary>
        /// <param name="eventArgs">Event arguments specifying which method is being executed, which are its arguments, and how should the execution continue after the execution of <b>OnEntry</b>.</param>
        public sealed override void OnEntry(MethodExecutionArgs eventArgs)
        {
            IConnectionProvider conn = GetConnectionProvider(eventArgs);
            if (conn != null)
            {                
                bool isLocalTran = OrmUtil.RequestTransaction(conn);
                SetIsLocalTran(eventArgs, isLocalTran);
            }
        }

        /// <summary>
        /// Rollbacks transaction and closes connection if it is local. Does nothing if connection-transaction context is shared. Executed when an exception occurs in the methods to which the current custom attribute has been applied. 
        /// </summary>
        /// <param name="eventArgs">Event arguments specifying which method is being executed, which are its arguments, and how should the execution continue after the execution of <b>OnEntry</b>.</param>
        public sealed override void OnException(MethodExecutionArgs eventArgs)
        {            
            IConnectionProvider conn = GetConnectionProvider(eventArgs);
            if (conn != null)
            {
                OrmUtil.CloseConnectionIfLocal(conn, GetIsLocalTran(eventArgs));
            }

            SetExceptionOccured(eventArgs, true);
        }

        /// <summary>
        /// Commits transaction and closes connection if it is local. Does nothing if connection-transaction context is shared. Executed <b>after</b> the body of methods to which this aspect is applied. 
        /// </summary>
        /// <param name="eventArgs">Event arguments specifying which method is being executed, which are its arguments, and how should the execution continue after the execution of <b>OnExit</b>.</param>
        public sealed override void OnExit(MethodExecutionArgs eventArgs)
        {
            if (GetExceptionOccured(eventArgs))
                return;

            IConnectionProvider conn = GetConnectionProvider(eventArgs);
            if (conn != null)
            {
                bool isLocalTran = GetIsLocalTran(eventArgs);
                OrmUtil.CommitTransactionIfLocal(conn, isLocalTran);
                OrmUtil.CloseConnectionIfLocal(conn, isLocalTran);
            }
        }

        private static void SetExceptionOccured(MethodExecutionArgs eventArgs, bool value)
        {
            AopExecutionToken aopControl = AopExecutionToken.EnsureMethodExecutionTag(eventArgs);
            aopControl.Items["TransactionRequiredAttribute.ExceptionOccured"] = value;
        }

        private static bool GetExceptionOccured(MethodExecutionArgs eventArgs)
        {
            AopExecutionToken aopControl = AopExecutionToken.EnsureMethodExecutionTag(eventArgs);
            bool exceptionOccured = aopControl.Items.ContainsKey("TransactionRequiredAttribute.ExceptionOccured")
                ? Convert.ToBoolean(aopControl.Items["TransactionRequiredAttribute.ExceptionOccured"])
                : false;
            return exceptionOccured;
        }

        private static bool GetIsLocalTran(MethodExecutionArgs eventArgs)
        {
            AopExecutionToken aopControl = AopExecutionToken.EnsureMethodExecutionTag(eventArgs);
            bool isLocalTran = aopControl.Items.ContainsKey("TransactionRequiredAttribute.IsLocalTran")
                ? Convert.ToBoolean(aopControl.Items["TransactionRequiredAttribute.IsLocalTran"])
                : false;
            return isLocalTran;
        }

        private static void SetIsLocalTran(MethodExecutionArgs eventArgs, bool value)
        {
            AopExecutionToken aopControl = AopExecutionToken.EnsureMethodExecutionTag(eventArgs);
            aopControl.Items["TransactionRequiredAttribute.IsLocalTran"] = value;
        }

        private IConnectionProvider GetConnectionProvider(MethodExecutionArgs eventArgs)
        {
            IConnectionProvider conn = GetConnectionProviderMethodArg(eventArgs);
            if (conn == null)
            {
                conn = GetConnectionProviderProperty(eventArgs);
            }

            if (conn == null && this.ThrowExceptionIfConnectionProviderCannotBeRetrieved)
                throw new InvalidOperationException("An instance of IConnectionProvider cannot be retrieved for method: " + eventArgs.Method.DeclaringType.FullName + "." + eventArgs.Method.Name);

            return conn;
        }

        private static IConnectionProvider GetConnectionProviderMethodArg(MethodExecutionArgs eventArgs)
        {
            object methodArgs = eventArgs.Arguments.ToArray();
            if (methodArgs == null)
                return null;

            foreach (object arg in eventArgs.Arguments.ToArray())
            {
                if (arg is IConnectionProvider)
                    return (IConnectionProvider)arg;
            }

            return null;
        }        

        private IConnectionProvider GetConnectionProviderProperty(MethodExecutionArgs eventArgs)
        {
            IConnectionProvider conn = null;
            if (eventArgs.Instance != null)
            {
                PropertyInfo prop = eventArgs.Instance.GetType().GetProperty(this.ConnectionProviderPropertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
                if (prop != null && prop.CanRead)
                {
                    conn = prop.GetValue(eventArgs.Instance, null) as IConnectionProvider;
                }
            }

            return conn;
        }

        #endregion
    }
}
